﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace MabiPatch
{
	/// <summary>
	/// Provides patching the client.
	/// </summary>
	public class Patcher
	{
		internal static PowerPatcherInterop.InitInfo info;
        internal static OfficialPatchInfo opi;
        public OfficialPatchInfo OPI { get { return opi; } }
        public Action updatePatchNumber;

		/// <summary>
		/// Constructor for the Patcher class
		/// </summary>
		/// <param name="MP">A delegate that takes care of updating the main progress display.</param>
		/// <param name="SPs">An array of delegates that update the sub progress displays.</param>
		public Patcher(PowerPatcherInterop.InitInfo _info)
		{
			info = _info;
            dlm = new DownloadManager("ftp://mabipatch.nexon.net", new Action<string>((s) => info.LogString(s, false)), info.SupUpdaters);
            opi = OfficialPatchInfo.GetFrom(new Uri("http://mabipatchinfo.nexon.net/patch/patch.txt"));
            if (!opi.PatchAccept)
                System.Windows.MessageBox.Show("Mabinogi is currently offline!", "Mabi Offline", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Exclamation);
		}
        ~Patcher()
        {
            Properties.Settings.Default.Save();
        }
        
        public void DownPatch(int ToVersion)
        {
            if (System.Windows.Forms.MessageBox.Show("Would you also like to clean your pack folder? (If in doubt, say yes)", "Clean?", System.Windows.Forms.MessageBoxButtons.YesNo, System.Windows.Forms.MessageBoxIcon.Information) == System.Windows.Forms.DialogResult.Yes)
            {
                string[] pFiles = Directory.GetFiles("package");
                foreach (string f in pFiles)
                    if (int.Parse(Common.GetLastVersion(f)) >= ToVersion)
                        File.Delete(f);
            }
        	//TODO: Use existing files (consideration of the DLLs)
        	Patch(ToVersion + "_full");
        }
		
		public void Patch()
		{
			Patch(Common.ReadVersion());
		}
		public void Patch(int CurrentVesion)
		{
			int ToVersion;
            if (Properties.Settings.Default.AllowPrepatch)
            {
            	List<int> vers = Common.GetAvailableVersions();
            	vers.Sort();
            	ToVersion = vers.Last();
            }
            else
            {
                ToVersion = opi.LocalVersion;
            }
			Patch(ToVersion, CurrentVesion);
		}
		public void Patch(int ToVersion, int FromVersion)
		{
            if (FromVersion >= ToVersion)
                return;
            Patch(Common.TraceVersions(FromVersion, ToVersion).ToArray());
		}
        public void Patch(params string[] VersionSequence)
        {
        	info.SetPatchState.Invoke(true);
        	try
        	{
	            foreach (string vStep in VersionSequence)
	            {
	                Dictionary<string, string> fTable;
	                downloadPatchFiles(vStep, out fTable);
                    downloadLangPack(vStep);
	                combinePatchFiles(vStep, fTable);
	                unzipFiles(vStep);
	                copyOut(vStep);
	                cleanup(vStep);
	                Common.WriteVersion(int.Parse(Common.GetLastVersion(vStep)));
                    updatePatchNumber();
	                info.MainUpdater(1, "Patch complete!");
	                info.SupUpdaters[0](1,"");
	                info.SupUpdaters[1](1,"");
	            }
        	} 
        	catch 
        	{
        		throw;
        	}
        	finally
        	{
        		info.SetPatchState.Invoke(false);
        	}
        }
        
        public void RedownloadLanFile()
        {
        	info.SetPatchState(true);
        	try
        	{
        		downloadLangPack(Common.ReadVersion() + "_full",System.IO.Path.Combine(Environment.CurrentDirectory, "package"));
        	}
        	finally
        	{
        		info.SetPatchState(false);
        	}
            info.MainUpdater(1,"Redownloading complete!");
            info.SupUpdaters[0](1, "");
            info.SupUpdaters[1](1, "");
        }

        private DownloadManager dlm;
        private void downloadPatchFiles(string basePatchName, out Dictionary<string, string> fTable)
        {
            string vFolder = Common.GetLastVersion(basePatchName);
            if (!Directory.Exists(basePatchName))
                Directory.CreateDirectory(basePatchName);
            if (File.Exists(string.Format("{0}\\{0}.txt", basePatchName)))
                File.Delete(string.Format("{0}\\{0}.txt", basePatchName));
            
            dlm.Reset();
            dlm.Add(new FileRequestInfo(string.Format("{0}\\{0}.txt", basePatchName), string.Format("{0}/{1}.txt", vFolder, basePatchName)));
            while (!dlm.AllDownloadsComplete)
                System.Threading.Thread.Sleep(33);

            int fileCount;
            fTable = new Dictionary<string, string>();
            using (StreamReader r = new StreamReader(basePatchName + "\\" + basePatchName + ".txt"))
            {
                fileCount = int.Parse(r.ReadLine());
                while (!r.EndOfStream)
                {
                    string tmp = r.ReadLine();
                    if (!tmp.Contains(","))
                        continue;
                    string[] parts = tmp.Split(new string[] {", "}, StringSplitOptions.RemoveEmptyEntries);
                    fTable.Add(parts[0], parts[2]);
                }
            }

            List<string> corruptFiles = VerifyFiles(basePatchName, new List<string>(fTable.Keys), fTable);

            while (corruptFiles.Count != 0)
            {
                downloadFileList(basePatchName, vFolder, corruptFiles);
                corruptFiles = VerifyFiles(basePatchName, corruptFiles, fTable);
            }
        }
        private void downloadFileList(string basePatchName, string vFolder, List<string> fTable)
        {
            dlm.Reset();
            foreach (string file in fTable)
            {
                dlm.Add(new FileRequestInfo(string.Format("{0}\\{1}", basePatchName, file), string.Format("{0}/{1}", vFolder, file)));
            }
            while (!dlm.AllDownloadsComplete)
            {
                info.MainUpdater.Invoke(dlm.Completed / (double)fTable.Count, "Downloading patch files (" + dlm.Completed + "/" + fTable.Count + ")");
                System.Windows.Forms.Application.DoEvents();
            }
        }
        private List<string> VerifyFiles(string BasePatchName, List<string> ToCheck, Dictionary<string, string> fTable)
        {
            List<string> corrupt = new List<string>();
            foreach (string file in ToCheck)
            {
            	info.SupUpdaters[0].Invoke(.5, "Checking " + file);
                string p = BasePatchName + "\\" + file;
                if ((!File.Exists(p)) || (Common.MD5(p) != fTable[file].ToLower()))
                {
                    corrupt.Add(file);
                    continue;
                }
            }
            return corrupt;
        }

        private void combinePatchFiles(string BasePatchName, Dictionary<string, string> fTable)
        {
            const int buffSize = 16384;
            using (FileStream fs = new FileStream(BasePatchName + "\\" + BasePatchName + ".zip", FileMode.Create))
            {
                byte[] buffer = new byte[buffSize];
                double completed = 0;
                foreach (string file in fTable.Keys)
                {
                    info.MainUpdater.Invoke(completed / (double)fTable.Keys.Count, "Adding " + file + " to the cumulative patch...");
                    using (FileStream part = new FileStream(BasePatchName + "\\" + file, FileMode.Open))
                    {
                        long tr;
                        int r;
                        r = part.Read(buffer, 0, buffSize);
                        tr = r;
                        while (r != 0)
                        {
                        	info.SupUpdaters[0].Invoke(tr / (double)part.Length, "(" + tr + "/" + part.Length + ")");
                            fs.Write(buffer, 0, r);
                            r = part.Read(buffer, 0, buffSize);
                            tr += r;
                        }
                    }
                    completed++;
                }
            }
        }

        private void downloadLangPack(string BasePatchName, string ExtractLocation)
        {
            dlm.Reset();
            string ptv = Common.GetLastVersion(BasePatchName);
            Directory.CreateDirectory(BasePatchName);
            dlm.Add(new FileRequestInfo(BasePatchName + "\\lang.zip", ptv + "/" + ptv + "_language.p_"));
            while (!dlm.AllDownloadsComplete)
                System.Windows.Forms.Application.DoEvents();
            using (Ionic.Zip.ZipFile zFile = Ionic.Zip.ZipFile.Read(BasePatchName + "\\lang.zip"))
            {
                zFile.ExtractProgress += new EventHandler<Ionic.Zip.ExtractProgressEventArgs>(zFile_ExtractProgress);
                zFile.ExtractAll(ExtractLocation, Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);
            }
            if (Properties.Settings.Default.DeleteZIPs)
                File.Delete(BasePatchName + "\\lang.zip");
        }

        private void downloadLangPack(string BasePatchName)
        {
            downloadLangPack(BasePatchName, BasePatchName + "\\content\\package");
        }

        private void unzipFiles(string BasePatchName)
        {
            string DN = BasePatchName + "\\content";
            if (Directory.Exists(DN))
                Directory.Delete(DN, true);
            using (Ionic.Zip.ZipFile zFile = Ionic.Zip.ZipFile.Read(BasePatchName + "\\" + BasePatchName + ".zip"))
            {
                zFile.ExtractProgress += new EventHandler<Ionic.Zip.ExtractProgressEventArgs>(zFile_ExtractProgress);
                zFile.ExtractAll(BasePatchName + "\\content", Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);
            }
        }

        void zFile_ExtractProgress(object sender, Ionic.Zip.ExtractProgressEventArgs e)
        {
            try
            {
                if (e.EventType == Ionic.Zip.ZipProgressEventType.Extracting_AfterExtractEntry || e.EventType == Ionic.Zip.ZipProgressEventType.Extracting_BeforeExtractEntry)
                {
                    info.MainUpdater.Invoke(e.EntriesExtracted / (double)e.EntriesTotal, "Extracting patch files... (" + e.EntriesExtracted + "/" + e.EntriesTotal + ")");
                    info.SupUpdaters[0].Invoke(e.BytesTransferred / (double)e.TotalBytesToTransfer, "Extracting " + e.CurrentEntry.FileName);
                }
                else
                    info.SupUpdaters[0].Invoke(e.BytesTransferred / (double)e.TotalBytesToTransfer, null);
            }
            catch
            {
            }
        }
        //NOTE: cheeck for full path
        private void copyOut(string BasePatchName)
        {
            string contentFolder = BasePatchName + "\\content";
            int t = Common.FindFiles(contentFolder).Count;
            //TODO: Threaded copying
            int c = 0;
            CopyFolder(contentFolder, Environment.CurrentDirectory, Path.Combine(Environment.CurrentDirectory, BasePatchName + "_deleted"), ref t, ref c);
        }
        private void CopyFolder(string sourceFolder, string destFolder, string bkpFolder, ref int cs, ref int ccs)
        {
            if (!Directory.Exists(destFolder))
                Directory.CreateDirectory(destFolder);
            string[] files = Directory.GetFiles(sourceFolder);
            foreach (string file in files)
            {
                string name = Path.GetFileName(file);
                string dest = Path.Combine(destFolder, name);
                info.SupUpdaters[0](0, "Copying " + name + " to " + destFolder);
                if (File.Exists(dest))
                {
                    string bkPath = Path.Combine(bkpFolder, name);
                    if (!Directory.Exists(bkpFolder))
                        Directory.CreateDirectory(bkpFolder);
                    if (File.Exists(bkPath))
                        File.Delete(bkPath);
                    File.Move(dest, bkPath);
                }
                File.Copy(file, dest, true);
                info.MainUpdater(ccs / (double)cs, "Copying out patch files... (" + ccs + "/" + cs + ")");
                ccs++;
            }
            string[] folders = Directory.GetDirectories(sourceFolder);
            foreach (string folder in folders)
            {
                string name = Path.GetFileName(folder);
                string dest = Path.Combine(destFolder, name);
                string bkp = Path.Combine(bkpFolder, name);
                CopyFolder(folder, dest, bkp, ref cs, ref ccs);
            }
        }
        private void cleanup(string BasePatchName)
        {
        	List<string> files = new List<string>();
        	if (Properties.Settings.Default.DeleteParts)
        	{
        		files.AddRange(Directory.GetFiles(BasePatchName,BasePatchName + ".*").ToList());
    			for (int i = 0; i < files.Count; i++)
    				if (files[i].EndsWith(".zip"))
    				    files.RemoveAt(i);
        	}
            if (Properties.Settings.Default.DeleteZIPs)
        	{
        		files.AddRange(Directory.GetFiles(BasePatchName, "*.zip", SearchOption.AllDirectories).ToList());
        	}
            if (Properties.Settings.Default.DeleteContent)
        	{
        		files.AddRange(Directory.GetFiles(BasePatchName + "\\content", "*.*", SearchOption.AllDirectories).ToList());
        	}
        	for (int i = 0; i < files.Count; i++)
        	{
        		info.MainUpdater(i / (double)files.Count,"Deleteing temporary files... (" + i + "/" + files.Count + ")");
        		info.SupUpdaters[0](0, files[i]);
        		File.Delete(files[i]);
        	}
        	if (Common.TreeEmpty(BasePatchName))
        		Directory.Delete(BasePatchName, true);
        }
	}


        public class OfficialPatchInfo
        {
            public bool PatchAccept { get; private set; }
            public int LocalVersion { get; private set; }
            public string LocalFTP { get; private set; }
            public int MainVersion { get; private set; }
            public string MainFTP { get; private set; }
            public int LauncherVersion { get; private set; }
            public string LoginIP { get; private set; }
            public string OtherClientArgs { get; private set; }
            public string Addin { get; private set; }
            public string Lang { get; private set; }

            public static OfficialPatchInfo Parse(string Info)
            {
                Dictionary<string, string> Parts = new Dictionary<string, string>();
                OfficialPatchInfo ret = new OfficialPatchInfo();
                using (System.IO.StringReader SR = new System.IO.StringReader(Info))
                {
                    while (SR.Peek() != -1)
                    {
                        string tmp = SR.ReadLine();
                        Parts.Add(tmp.Split(new char[] { '=' }, 2)[0], tmp.Split(new char[] { '=' }, 2)[1]);
                    }
                }
                ret.PatchAccept = (Parts["patch_accept"] == "1" ? true : false);
                ret.LocalVersion = int.Parse(Parts["local_version"]);
                ret.LocalFTP = Parts["local_ftp"];
                ret.MainVersion = int.Parse(Parts["main_version"]);
                ret.MainFTP = Parts["main_ftp"];
                ret.LauncherVersion = int.Parse(Parts["launcherinfo"]);
                ret.LoginIP = Parts["login"];
                ret.OtherClientArgs = Parts["arg"];
                ret.Addin = Parts["addin"];
                ret.Lang = Parts["lang"];
                return ret;
            }
            /// <summary>
            /// Actually retrives and parses the data from the supplied URL
            /// </summary>
            /// <param name="URL"></param>
            public static OfficialPatchInfo GetFrom(Uri URL)
            {
                using (System.Net.WebClient wc = new System.Net.WebClient())
                {
                    return OfficialPatchInfo.Parse(wc.DownloadString(URL));
                }
            }
            public override string ToString()
            {
                return string.Format(@"patch_accept={0}
local_version={1}
local_ftp={2}
main_version={3}
main_ftp={4}
launcherinfo={5}
login={6}
arg={7}
addin={8}
lang={9}", Convert.ToInt32(PatchAccept), LocalVersion, LocalFTP, MainVersion, MainFTP, LauncherVersion, LoginIP, OtherClientArgs, Addin, Lang);
            }
        }
}
